//-----------------------------------------------------------------------------------------------
// Author: Andre Van Der Merwe <dart@pobox.com>
// Project: .net Compound File (http://dotnetpcf.sourceforge.net)
//
// Modifications
//    2001-02-27: (AVDM) Started using NUnit (http://nunit.sourceforge.net)
//                        Removed all old tests they needed to much rewriting to be useful. New 
//                        tests will need to be written.
// 
// Copyright (c) Andre Van Der Merwe 2000-2001
// Licence: BSD - see the licence.txt file or visit the project homepage
//-----------------------------------------------------------------------------------------------


//TODO only read current block on first read OR write (unless writing full block!)

namespace dotnetpcf.CompoundFile
{
   using System;
   using System.Diagnostics;
   using System.Collections;
   using System.Data;
   using System.IO;

   /// <summary>
   /// Makes a file, which can span many non-sequential blocks, appear as a single contigious stream of data.
   /// </summary>
   /// <remarks>
   /// The FSFilter stream has a in memory cache that is big enough to hold exactly one file block. All reads
   /// or writes are read to/from the cache. The cache is flushed to the physical stream when moving from one
   /// block to the next or when the file is closed. It is very important that the user remembers to call 
   /// .Close() on this stream, if the dont the cache will not be saved. Unlike other classes derived from the
   /// stream class, calling .Close() will not close the physical stream as the physical stream has many filter 
   /// streams
   /// </remarks>
   public class FSFilterStream : Stream
   {
      internal FSFilterStream(  Stream PhysicalStream,  FileTable.FileTableEntry fent  ) : base()
      {
         Debug.Assert(  (null != PhysicalStream),  "Physical stream can not be null"  );
         Debug.Assert(  (null != fent),  "File Table Entry can not be null"  );

         m_FileTableEntry = fent;
         m_PhysicalStream = PhysicalStream;
         m_iVirtualSize = fent.m_iLength;
         Debug.Assert(  m_iVirtualSize <= m_PhysicalStream.Length,  "FSFilerStream:ctor virtual stream size can not be larger than physical stream size"  );

            //Calc the start offset of this stream
         m_iStreamBase = fent.m_iID * SysInfo.BLOCK_BYTE_SIZE;

         MoveToBlock(  0  );
      }

      private readonly long m_iStreamBase;
      private Stream m_PhysicalStream = null;
      private FileTable.FileTableEntry m_FileTableEntry = null;
      private long m_iVirtualPosition = 0;
      private long m_iVirtualSize = 0;
      private uint m_iVirtualBlockIndex = SysInfo.UNDEFINED;
      private uint m_iPhysicalBlockIndex = SysInfo.UNDEFINED;
      private long m_iPositionInCurrentBlock = 0;
      private byte[] m_CurrentBlock = new byte[ SysInfo.BLOCK_BYTE_SIZE ];
      private bool m_bModified = false;

      private void SetVirtualSize(  long iSize  )
      {
         m_iVirtualSize = iSize;
         m_FileTableEntry.m_iLength = (uint)m_iVirtualSize;
      }

      void MoveToBlock(  uint iBlock  )
      {
            //Ignore if trying to move to current block
         if(  iBlock == m_iVirtualBlockIndex  )
            return;

         WriteCache();

         m_iVirtualBlockIndex = iBlock;
 
         uint iBlockCount = m_FileTableEntry.FileTableOwner.GetBlockCount(  m_FileTableEntry  );
         Debug.Assert(  0 != iBlockCount  );

         if(  iBlockCount < m_iVirtualBlockIndex + 1  )
            m_FileTableEntry.FileTableOwner.EnsureFileHasNBlocks(  m_FileTableEntry,  m_iVirtualBlockIndex + 1  );

            //The 0th block is always the CF header, no FSFilterStream should ever be created on this block
         m_iPhysicalBlockIndex = m_FileTableEntry.FileTableOwner.GetNthBlock(  m_FileTableEntry,  m_iVirtualBlockIndex  );
         Debug.Assert(  (0 != m_iPhysicalBlockIndex), "trying to create a file stream on the 0th physical block"  );

         //TODO throw exception if on 0th block

         long iPos = m_iPhysicalBlockIndex * SysInfo.BLOCK_BYTE_SIZE;

            //Make sure that the physical file is big enough
         if(  iPos > m_PhysicalStream.Length  )
            m_PhysicalStream.SetLength(  iPos  );

         
            //Only seek if nessasary
         if(  m_PhysicalStream.Position != iPos  )
            m_PhysicalStream.Position = iPos;

         m_PhysicalStream.Read(  m_CurrentBlock,  0,  SysInfo.BLOCK_BYTE_SIZE  );
      }

      private void WriteCache()
      {
         if(  m_bModified  )
         {
              //TODO WriteCache() - do need to save position? What about if allowing multi-threading?
            long iPos = m_PhysicalStream.Position;

            m_PhysicalStream.Position = SysInfo.BLOCK_BYTE_SIZE * m_iPhysicalBlockIndex;
            m_PhysicalStream.Write(  m_CurrentBlock,  0,  SysInfo.BLOCK_BYTE_SIZE  );
            m_PhysicalStream.Position = iPos;

            m_bModified = false;
         }
      }

      protected void SetPosition(  long iPos  )
      {
         //TODO throw exception if more than one byte past end of physical stream

         MoveToBlock(  (uint)iPos / SysInfo.BLOCK_BYTE_SIZE  );
         m_iPositionInCurrentBlock = iPos - (m_iVirtualBlockIndex * SysInfo.BLOCK_BYTE_SIZE);
         m_iVirtualPosition = iPos;
      }

      public override int Read(  byte[] buffer,  int offset,  int count  )
      {
            //Get the maximun number of bytes that can be read
         count = (int)Math.Min(  count,  (Length - m_iVirtualPosition)  );

         long iStart = 0;
         long iBytesLeft = count;

         if(  0 == count  )
            return 0;

         while(  0 != iBytesLeft  )
         {
            long iCanRead = SysInfo.BLOCK_BYTE_SIZE - m_iPositionInCurrentBlock;

            iCanRead = Math.Min(  iCanRead,  iBytesLeft  );

            Buffer.BlockCopy(  m_CurrentBlock,  (int)m_iPositionInCurrentBlock,  buffer,  (int)iStart,  (int)iCanRead  );

            Position += iCanRead;               
            iStart += iCanRead;
            iBytesLeft -= iCanRead;
         }

         return count;
      }

      public override void Write(  byte[] buffer,  int offset,  int count  )
      {
         m_FileTableEntry.FileTableOwner.EnsureFileHasNBlocks(  m_FileTableEntry,  (uint)(count / SysInfo.BLOCK_BYTE_SIZE + ((0 == count % SysInfo.BLOCK_BYTE_SIZE) ? 0 : 1))  );

         long iBytesLeft = count;
         long iStart = 0;
         

         while(  0 != iBytesLeft  )
         {
            long iCanWrite = SysInfo.BLOCK_BYTE_SIZE - m_iPositionInCurrentBlock;

               //Block has been modified. NB must set this in the while loop as it can be checked when
               // changing the Position
            m_bModified = true;

            iCanWrite = Math.Min(  iCanWrite,  iBytesLeft  );

            Buffer.BlockCopy(  buffer,  (int)iStart,  m_CurrentBlock,  (int)m_iPositionInCurrentBlock,  (int)iCanWrite  );
            Position += iCanWrite;

            iStart += iCanWrite;
            iBytesLeft -= iCanWrite;
         }
         

            //Update the virtual block size
         SetVirtualSize(  Math.Max(  m_iVirtualSize,  (m_iVirtualBlockIndex * SysInfo.BLOCK_BYTE_SIZE) + m_iPositionInCurrentBlock )   );

         //return count;
      }

      public override void Close()
      {
            //Flush this stream, but do not close the physical stream
         Flush();

         Position = 0;
      }

      public override void Flush()
      {
         WriteCache();
         m_PhysicalStream.Flush();
      }

      public override long Seek(  long offset,  SeekOrigin origin  )
      {
         switch(  origin  )
         {
            case SeekOrigin.Begin:
            {
               if(  (offset > Length + 1) || (offset < 0)  )
                  return Position;

               Position = offset;
               break;
            }

            case SeekOrigin.Current:
            {
               long iTmp = Position + offset;

               if(  (iTmp > Length + 1) || (iTmp < 0)  )
                  return Position;

               Position = iTmp;
               break;
            }

            case SeekOrigin.End:
            {
               long iTmp = Length + offset;

               if(  (iTmp > Length + 1) || (iTmp < 0)  )
                  return Position;

               Position = iTmp;
               break;
            }
         }

         return Position;
      }

      public override void SetLength(  long value  )
      {
         m_bModified = true;
         m_FileTableEntry.FileTableOwner.EnsureFileHasNBlocks(  m_FileTableEntry,  (uint)(value / SysInfo.BLOCK_BYTE_SIZE)  );
         SetVirtualSize(  value  );
      }

      //public override bool DataAvailable 
      //{
      //      //To be removed after beta 1 
      //   override get{ return m_PhysicalStream.DataAvailable;  }
      //}


      public override long Length
      { 
         get{ return m_iVirtualSize; }
      }

      public override long Position
      { 
         get{ return m_iVirtualPosition; }
         set{ SetPosition(  value  ); }
      }

      public override bool CanRead
      { 
         get{ return m_PhysicalStream.CanRead; }
      }

      public override bool CanSeek
      { 
         get{ return m_PhysicalStream.CanSeek; }
      }

      public override bool CanWrite
      { 
         get{ return m_PhysicalStream.CanWrite; }
      }

      public long PhysicalStreamPosition
      {
         get
         { 
            return (m_iPhysicalBlockIndex * SysInfo.BLOCK_BYTE_SIZE) + m_iPositionInCurrentBlock;
         }
      }


      ////---------------------------------------------------------------------------------
      //// Tests
      ////------------

      //public class FSFilterStreamTestsOnly : NUnit.Framework.TestCase
      //{  
      //   public FSFilterStreamTestsOnly(  string s  ) : base( s ) {}

      //   public static NUnit.Framework.ITest Suite
      //   {
      //      get{ return new NUnit.Framework.TestSuite(  typeof(FSFilterStreamTests)  ); }
      //   }
      //}


      //public class FSFilterStreamTests : NUnit.Framework.TestCase
      //{
      //   public FSFilterStreamTests(  string s  ) : base( s ) {}

      //   protected override void SetUp() {}

      //   protected override void TearDown() {}

      //   //Test somthing here
      //}
   }
}
